/**
 * 
 */
package abel.wlmr.server;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import abel.util.HexString;
import abel.util.StdBcdNumber;
import abel.wlmr.config.MeterStation;
import abel.wlmr.config.MsaConfig;
import abel.wlmr.config.MsaConfigLoader;
import abel.wlmr.config.PacketCounter;
import abel.wlmr.config.PacketSender;
import abel.wlmr.config.PacketSender.PacketTimes;
import abel.wlmr.model.WlmrBasePart;
import abel.wlmr.model.WlmrBaseUnit;
import abel.wlmr.model.WlmrEmptyPart;
import abel.wlmr.model.WlmrPacket;
import abel.wlmr.model.type.AfnType;
import abel.wlmr.model.type.C1DateTimeType;
import abel.wlmr.model.type.DataIdBody;
import abel.wlmr.model.type.FnActiveType;
import abel.wlmr.model.type.FnProactiveType;
import abel.wlmr.model.type.MeterConfigBody;
import abel.wlmr.model.type.PeriodUnitType;
import abel.wlmr.model.type.WlmrControlField.CtrlAcd;
import abel.wlmr.model.type.WlmrControlField.CtrlDir;
import abel.wlmr.model.type.WlmrControlField.CtrlPrm;
import abel.wlmr.model.unit.Afn00Part;
import abel.wlmr.model.unit.Afn02F3Unit;
import abel.wlmr.model.unit.Afn02Part;
import abel.wlmr.model.unit.Afn04DownPart;
import abel.wlmr.model.unit.Afn04F401Unit;
import abel.wlmr.model.unit.Afn04F6566Unit;
import abel.wlmr.model.unit.Afn05DownPart;
import abel.wlmr.model.unit.Afn05F31Unit;
import abel.wlmr.model.unit.Afn09F1UpUnit;
import abel.wlmr.model.unit.Afn09UpPart;
import abel.wlmr.model.unit.AfnFirstPart;
import abel.wlmr.model.unit.AfnQueryPart;
import abel.wlmr.model.unit.AfnSecondDownPart;
import abel.wlmr.model.unit.AfnSecondF401Unit;
import abel.wlmr.model.unit.AfnSecondF409DownUnit;
import abel.wlmr.model.unit.AfnSecondF409Unit;
import abel.wlmr.model.unit.AfnSecondF410DownUnit;
import abel.wlmr.model.unit.AfnSecondF410Unit;
import abel.wlmr.model.unit.AfnSecondPart;
import abel.wlmr.model.unit.AfnThirdDownPart;
import abel.wlmr.model.unit.AfnThirdF1_2DownUnit;
import abel.wlmr.model.unit.AfnThirdF1_2Unit;
import abel.wlmr.model.unit.AfnThirdPart;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.Attribute;

/**
 * @author Abel
 *
 */
public class WlmrServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * confirm if time diff between main & station excceds this value;
     * deny otherwise.
     */
    private static final int ALLOWED_DIFF_MINUTES = 30;

    private Logger log;

    public WlmrServerHandler() {
        MsaConfig msaConfig = MsaConfigLoader.getMsaConfig();
        log = LoggerFactory.getLogger(msaConfig.getMain().getA1().getAsc());
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#channelActive(io.netty.channel.ChannelHandlerContext)
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("connection ({}) from {}", ctx.name(), ctx.channel().remoteAddress());

        // get config instance.
        MsaConfig msaCfg = MsaConfigLoader.getMsaConfig();

        // init sequence.
        Attribute<Byte> seq = ctx.channel().attr(PacketConsts.sequenceKey);
        seq.setIfAbsent((byte) 0);

        // init sender
        Attribute<PacketSender> sender = ctx.channel().attr(PacketConsts.senderKey);
        sender.setIfAbsent(new PacketSender());

        // init configured key - for meter config
        ctx.channel().attr(PacketConsts.lastQueryKey).setIfAbsent(new DateTime(2016, 1, 1, 0, 0));
        ctx.channel().attr(PacketConsts.lastConfigureKey).setIfAbsent(new DateTime(2016, 1, 1, 0, 0));
        ctx.channel().attr(PacketConsts.lastFirstTaskKey).setIfAbsent(new DateTime(2016, 1, 1, 0, 0));
        ctx.channel().attr(PacketConsts.lastFirstDataKey).setIfAbsent(new DateTime(2016, 1, 1, 0, 0));
        ctx.channel().attr(PacketConsts.lastSecondTaskKey).setIfAbsent(new DateTime(2016, 1, 1, 0, 0));
        ctx.channel().attr(PacketConsts.lastSecondDataKey).setIfAbsent(new DateTime(2016, 1, 1, 0, 0));

        // init statistics key
        ctx.channel().attr(PacketConsts.counterKey).setIfAbsent(new PacketCounter());

        // will request station info in next idle.
        {
            WlmrPacket r = new WlmrPacket(log);
            r.getAddr().setA1(msaCfg.getMain().getA1());
            r.getAddr().getA3().setAddr(msaCfg.getMain().getA3().getAddr());

            r.getControl().setDir(CtrlDir.Down);
            r.getControl().setPrm(CtrlPrm.Active);
            r.getControl().setFat(FnActiveType.UserData);

            r.setAfn(AfnType.Term);
            r.getFseq().setCon(true);
            r.getFseq().setTpv(true);

            // empty part is enough for query station
            WlmrEmptyPart part1 = new WlmrEmptyPart();
            part1.getId().getDt().setTypeList(Afn09UpPart.F_TERM_INFO);
            r.setPart1(part1);

            // sender
            sender.get().queuePacket(r);
            log.info("queue 'query station' for {}", ctx.name());
        }
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#channelRead(io.netty.channel.ChannelHandlerContext, java.lang.Object)
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // get log for current station
        MeterStation station = ctx.channel().attr(PacketConsts.stationKey).get();
        if (station != null) {
            log = LoggerFactory.getLogger(station.getAddr().stationKey());
        }

        // check type
        if (!(msg instanceof WlmrPacket)) {
            log.error("UP unrecognized : {}", msg);
            return;
        }
        WlmrPacket p = (WlmrPacket) msg;

        // statistics
        ctx.channel().attr(PacketConsts.counterKey).get().incRecvCount();

        // verify
        if (p.getControl().getDir() != CtrlDir.Up) {
            log.error("Are you sure it isn't Up: {}", p.getControl().getDir());
        }

        // get station & main
        MsaConfig msaConfig = MsaConfigLoader.getMsaConfig();
        station = MsaConfigLoader.getStationMap().get(p.getAddr().stationKey());

        // set to attribute if not null
        if (station != null) {
            ctx.channel().attr(PacketConsts.stationKey).set(station);

            // build log.
            /*
            if (!ctx.channel().attr(PacketConsts.logBuiltKey).get()) {
                buildLogForStataion(station);
                ctx.channel().attr(PacketConsts.logBuiltKey).set(true);
            }
            */

            log.warn("registered meter station {} with pwd {} for context {}.",
                station.getAddr().stationKey(), station.getPwd(), ctx.name());

            // queue Query meters.
            Attribute<DateTime> lastQuery = ctx.channel().attr(PacketConsts.lastQueryKey);
            if (new Duration(lastQuery.get(), DateTime.now()).getStandardMinutes() > msaConfig.getQueryPeriod()) {
                queueQueryMeters(ctx);

                // change configured
                lastQuery.set(DateTime.now());
            }
        } else {
            log.warn("meter station un-registered: {}.", p.getAddr().stationKey());
        }

        // if there's any events.
        if (p.getControl().getAcd() == CtrlAcd.HasEvents
            && (p.getEventCount().getEc1() > 0 || p.getEventCount().getEc2() > 0)) {
            queueEvents(ctx, p);
        }

        // if response (may be Confirm / First / Second / ...)
        if (p.getControl().getPrm() == CtrlPrm.Proactive) {
            PacketTimes req = ctx.channel().attr(PacketConsts.senderKey).get().gotReply(p.getFseq().getSeq());

            if (req != null) {
                log.info("SEQ {}: Up {} for Down {}.",
                    p.getFseq().getSeq(), p.getAfn(), req.getPacket().getAfn());

                dealReqResp(ctx, req.getPacket(), p);
            }
        }

        // check if to send sync time
        if (p.getFseq().isTpv() && p.getTpv() != null && p.getTpv().getSendTime() != null) {
            if (new Duration(p.getTpv().getSendTime().toMeterC16Joda(), DateTime.now()).getStandardHours() >= 2) {
                log.warn("Need to Ctrl-Sync for {} caused by max time diff > 2 hours.", station.getAddr().stationKey());
                queueCtrlSync(ctx, p);
            }
        }

        // check & reply.
        switch (p.getAfn()) {
        case Verify:
            // just does nothing now. Need to record it in future.
            break;

        case Heart:
            dealHeart(ctx, p);
            break;

        case Ctrl:
            dealCtrl(ctx, p);
            break;

        case First:
            dealFirst(ctx, p);
            break;

        case Second:
            dealSecond(ctx, p);
            break;

        case Third:
            dealThird(ctx, p);
            break;

        case Term:
            dealTerm(ctx, p);
            break;

        case Query:
            // check gotten meter info.
            dealQueryResponse(ctx, p);
            break;

        default:
            log.info("No need to reply to {}", ctx.name());
            break;
        }

    }

    /**
     * Deal request & response packets.
     * e.g., Down Query but got Up Confirm, then down Set.
     * But if not then transferred to next deal method.
     * 
     * @param ctx
     * @param req
     * @param resp
     */
    private void dealReqResp(ChannelHandlerContext ctx, WlmrPacket req, WlmrPacket resp) {
        if (req.getAfn() == AfnType.Query && resp.getAfn() != AfnType.Query) {
            // just set.
            log.info("queue 'Set' meters because of un-expected {} for Down Query", resp.getAfn());
            queueSetMeterList(ctx, req);
        }
    }

    private void queueCtrlSync(ChannelHandlerContext ctx, WlmrPacket p) {
        MsaConfig msaCfg = MsaConfigLoader.getMsaConfig();

        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(msaCfg.getMain().getA1());
        // use station A2.
        r.getAddr().setA2(p.getAddr().getA2());
        r.getAddr().getA3().setAddr(msaCfg.getMain().getA3().getAddr());

        r.getControl().setDir(CtrlDir.Down);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.UserData);

        r.setAfn(AfnType.Ctrl);
        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        // set passwd.
        Attribute<MeterStation> station = ctx.channel().attr(PacketConsts.stationKey);
        if (station != null) {
            r.setPwd(station.get().getPwd());
        }

        // part1
        Afn05DownPart part1 = new Afn05DownPart();
        part1.getId().getDt().setTypeList(Afn05DownPart.F_SYNC);

        r.setPart1(part1);

        // unit F31
        Afn05F31Unit u31 = new Afn05F31Unit();
        u31.setStdTime(new C1DateTimeType(DateTime.now(), true));

        part1.firstUnit(u31);

        ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(r);

        log.info("queue {} for {} because of max time diff between station and server.",
            r.getAfn(), station.get() == null ? "none" : station.get().getAddr().stationKey());
    }

    private void dealCtrl(ChannelHandlerContext ctx, WlmrPacket p) {
        MsaConfig msaCfg = MsaConfigLoader.getMsaConfig();

        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(msaCfg.getMain().getA1());
        // use station A2.
        r.getAddr().setA2(p.getAddr().getA2());
        // 响应帧MSA应为0.
//        r.getAddr().getA3().setAddr(msaCfg.getMain().getA3().getAddr());

        r.getControl().setDir(CtrlDir.Down);
        r.getControl().setPrm(CtrlPrm.Proactive);
        r.getControl().setPat(FnProactiveType.UserData);

        r.setAfn(AfnType.Ctrl);

        r.getFseq().setCon(false);
        r.getFseq().setTpv(true);
        r.getFseq().setSeq(p.getFseq().getSeq());

        // set passwd.
        Attribute<MeterStation> station = ctx.channel().attr(PacketConsts.stationKey);
        if (station != null) {
            r.setPwd(station.get().getPwd());
        }

        // set tpv.
        r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

        // part1
        Afn05DownPart part1 = new Afn05DownPart();
        part1.getId().getDt().setTypeList(Afn05DownPart.F_SYNC);

        r.setPart1(part1);

        // unit F31
        Afn05F31Unit u31 = new Afn05F31Unit();
        u31.setStdTime(new C1DateTimeType(DateTime.now(), true));

        part1.firstUnit(u31);

        // send out
        ctx.writeAndFlush(r);
    }

    /**
     * Queue task to query all meters.
     * 
     * @param ctx
     */
    private void queueQueryMeters(ChannelHandlerContext ctx) {
        // retrieve one meter.
        MeterStation station = ctx.channel().attr(PacketConsts.stationKey).get();
        if (station.getMeterList().isEmpty()) {
            return;
        }

        int group = -1;
        List<MeterConfigBody> mlist = new ArrayList<>();
        for (MeterConfigBody m1 : station.getMeterList()) {
            if ((m1.getDaNo() - 1) / 8 == group) {
                mlist.add(m1);
            } else {
                // build one packet.
                if (!mlist.isEmpty()) {
                    buildAndQueueMeterGroup(ctx, mlist, station);
                }

                // change group
                group = (m1.getDaNo() - 1) / 8;
                mlist = new ArrayList<>();
                mlist.add(m1);
            }
        }

        // build last packet.
        if (!mlist.isEmpty()) {
            buildAndQueueMeterGroup(ctx, mlist, station);
        }
    }

    private void buildAndQueueMeterGroup(ChannelHandlerContext ctx, List<MeterConfigBody> mlist, MeterStation station) {
        MsaConfig msaCfg = MsaConfigLoader.getMsaConfig();

        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(msaCfg.getMain().getA1());
        // use station A2.
        r.getAddr().setA2(station.getAddr().getA2());
        r.getAddr().getA3().setAddr(msaCfg.getMain().getA3().getAddr());

        r.getControl().setDir(CtrlDir.Down);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.UserData);

        r.setAfn(AfnType.Query);
        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        // build array for meter list.
        short[] daList = new short[mlist.size()];
        int cnt = 0;
        for (MeterConfigBody m1 : mlist) {
            daList[cnt++] = m1.getDaNo();
        }

        // empty part is enough to query meter.
        WlmrEmptyPart part1 = new WlmrEmptyPart();
        part1.getId().getDa().setAddrList(daList);
        part1.getId().getDt().setTypeList(AfnQueryPart.F_METER_CONFIG, AfnQueryPart.F_EXCEPTION,
            AfnQueryPart.F_SETTING);

        r.setPart1(part1);

        // queue
        ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(r);

        log.info("queue {} for {} meters", r.getAfn(), mlist.size());
    }

    /**
     * Deal First request from meter station.
     * 
     * @param ctx
     * @param p
     * @param station
     * @param r
     */
    private void dealFirst(ChannelHandlerContext ctx, WlmrPacket p) {
        // check if confirm
        if (p.getControl().getPrm() == CtrlPrm.Active) {
            MsaConfig cfg = MsaConfigLoader.getMsaConfig();
            boolean confirmed = new Random().nextBoolean();

            WlmrPacket r = new WlmrPacket(log);

            r.getAddr().setA1(cfg.getMain().getA1());
            // use station A2.
            r.getAddr().setA2(p.getAddr().getA2());
            // 响应帧MSA应为0.
//            r.getAddr().getA3().setAddr(cfg.getMain().getA3().getAddr());

            r.setAfn(AfnType.Verify);

            r.getControl().setDir(CtrlDir.Down);
            r.getControl().setPrm(CtrlPrm.Proactive);
            r.getControl().setPat(confirmed ? FnProactiveType.Confirm : FnProactiveType.Deny);

            r.getFseq().setTpv(true);
            r.getFseq().setSeq(p.getFseq().getSeq());
            r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

            // part 1
            Afn00Part part1 = new Afn00Part();
            part1.getId().getDt().setTypeList(confirmed ? Afn00Part.F_CONFIRM : Afn00Part.F_DENY);

            r.setPart1(part1);

            // send.
            ctx.writeAndFlush(r);
        }

        // process part one by one
        for (WlmrBasePart p1 : p.getPartList()) {
            AfnFirstPart part1 = (AfnFirstPart) p1;

            for (List<WlmrBaseUnit> units : part1.getUnitList()) {
                for (WlmrBaseUnit u1 : units) {
                    // log.
                    log.info("Got {} for {}: {}.", p.getAfn(), part1.getId(), u1);
                }
            }
        } // part list
    }

    /**
     * Deal Second data, Confirm to Active only.
     */
    private void dealSecond(ChannelHandlerContext ctx, WlmrPacket p) {
        // check if confirm
        if (p.getControl().getPrm() == CtrlPrm.Active) {
            MsaConfig cfg = MsaConfigLoader.getMsaConfig();
            boolean confirmed = new Random().nextBoolean();

            WlmrPacket r = new WlmrPacket(log);

            r.getAddr().setA1(cfg.getMain().getA1());
            // use station A2.
            r.getAddr().setA2(p.getAddr().getA2());
            // 响应帧MSA应为0.
//            r.getAddr().getA3().setAddr(cfg.getMain().getA3().getAddr());

            r.setAfn(AfnType.Verify);

            r.getControl().setDir(CtrlDir.Down);
            r.getControl().setPrm(CtrlPrm.Proactive);
            r.getControl().setPat(confirmed ? FnProactiveType.Confirm : FnProactiveType.Deny);

            r.getFseq().setTpv(true);
            r.getFseq().setSeq(p.getFseq().getSeq());
            r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

            // part 1
            Afn00Part part1 = new Afn00Part();
            part1.getId().getDt().setTypeList(confirmed ? Afn00Part.F_CONFIRM : Afn00Part.F_DENY);

            r.setPart1(part1);

            // send.
            ctx.writeAndFlush(r);
        }

        for (WlmrBasePart p1 : p.getPartList()) {
            AfnSecondPart part1 = (AfnSecondPart) p1;

            for (List<WlmrBaseUnit> ulst : part1.getUnitList()) {
                for (WlmrBaseUnit unit1 : ulst) {
                    if (unit1 instanceof AfnSecondF409Unit) {
                        AfnSecondF409Unit u1 = (AfnSecondF409Unit) unit1;

                        // log
                        log.info("day consuming water for meter {}: flow = {}, tdd = {}, reading time = {}.",
                            u1.getOuterId().getDa(), u1.getForwardFLow(), u1.getFrozeTdd(), u1.getReadingTime());
                    } else if (unit1 instanceof AfnSecondF410Unit) {
                        AfnSecondF410Unit u1 = (AfnSecondF410Unit) unit1;

                        // log
                        log.info("month consumed water for meter {}: flow = {}, tdm = {}, reading time = {}.",
                            u1.getOuterId().getDa(), u1.getForwardFLow(), u1.getFrozeTdm(), u1.getReadingTime());
                    } else if (unit1 instanceof AfnSecondF401Unit) {
                        AfnSecondF401Unit u1 = (AfnSecondF401Unit) unit1;

                        // log
                        log.info("now consumed water for meter {}: tdc = {}, flow list = {}.",
                            u1.getOuterId().getDa(), u1.getCurveTdc(), u1.getFlowPointList());
                    } else {
                        log.info("un-processed unit: {}", unit1);
                    }
                }
            }
        }
    } // dealSecond

    private void dealThird(ChannelHandlerContext ctx, WlmrPacket p) {
        // Confirm to Active only.
        if (p.getControl().getPrm() == CtrlPrm.Active) {
            MsaConfig msaCfg = MsaConfigLoader.getMsaConfig();

            WlmrPacket r = new WlmrPacket(log);
            r.getAddr().setA1(msaCfg.getMain().getA1());
            // use station A2.
            r.getAddr().setA2(p.getAddr().getA2());
            // 响应帧MSA应为0.
//            r.getAddr().getA3().setAddr(msaCfg.getMain().getA3().getAddr());

            r.setAfn(AfnType.Verify);

            r.getControl().setDir(CtrlDir.Down);
            r.getControl().setPrm(CtrlPrm.Proactive);
            r.getControl().setPat(FnProactiveType.Confirm);

            r.getFseq().setTpv(true);
            r.getFseq().setSeq(p.getFseq().getSeq());

            r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

            // part1
            Afn00Part part1 = new Afn00Part();
            r.setPart1(part1);

            part1.getId().getDt().setTypeList(Afn00Part.F_CONFIRM);

            ctx.writeAndFlush(r);
        }

        for (WlmrBasePart p1 : p.getPartList()) {
            AfnThirdPart part1 = (AfnThirdPart) p1;

            for (List<WlmrBaseUnit> ulist : p1.getUnitList()) {
                for (WlmrBaseUnit unit1 : ulist) {
                    AfnThirdF1_2Unit u1 = (AfnThirdF1_2Unit) unit1;

                    // log
                    log.info("event count for {}: {}",
                        part1.getId().getDt().firstType(), u1);
                }
            }
        }
    }

    /**
     * If all of meter info are same as config, so nothing happen.
     * If any one of meter is different, so config all meters to the station.
     * 
     * @param ctx
     * @param p
     */
    private void dealQueryResponse(ChannelHandlerContext ctx, WlmrPacket p) {
        Attribute<MeterStation> station = ctx.channel().attr(PacketConsts.stationKey);

        // check if meters are same.
        boolean changed = false;
        for (WlmrBasePart part1 : p.getPartList()) {
            if (part1 instanceof AfnQueryPart) {
                for (WlmrBaseUnit unit1 : part1.flatUnitList()) {
                    if (unit1 instanceof Afn04F401Unit) {
                        Afn04F401Unit u401 = (Afn04F401Unit) unit1;
                        if (u401.getMeterCnt() > 0) {
                            MeterConfigBody m1 = u401.getMeterList().get(0);

                            // check if same meter.
                            for (MeterConfigBody m2 : station.get().getMeterList()) {
                                if (m1.getMeterNo() != m2.getMeterNo()
                                    || m1.getDaNo() != m2.getDaNo()
                                    || !m1.getMeterAddr().equals(m2.getMeterAddr())) {
                                    changed = true;
                                    log.warn("Changed meter: Old DA-No: {}, New DA-No: {}, Old Addr: {}, New Addr: {}.",
                                        m1.getDaNo(), m2.getDaNo(), m1.getMeterAddr(), m2.getMeterAddr());

                                    break;
                                }
                            }
                        }

                        // at least 1 meter is changed.
                        if (changed) {
                            break;
                        }
                    }
                }
            }

            // at least 1 meter is same.
            if (changed) {
                break;
            }
        }

        // all not changed.
        if (!changed) {
            log.info("None of meters is changed.");
            return;
        }
    }

    /**
     * @param ctx
     * @param req
     * @param station
     */
    public void queueSetMeterList(ChannelHandlerContext ctx, WlmrPacket req) {
        Attribute<MeterStation> station = ctx.channel().attr(PacketConsts.stationKey);
        if (station.get() == null) {
            log.warn("un-login or un-registered station: {}", req != null ? req.getAddr().stationKey() : ctx.name());
            return;
        }

        Attribute<PacketSender> sender = ctx.channel().attr(PacketConsts.senderKey);

        int group = -1;
        List<MeterConfigBody> list = new ArrayList<>();
        for (MeterConfigBody m1 : station.get().getMeterList()) {
            if (group == (m1.getDaNo() - 1) / 8) {
                list.add(m1);
            } else {
                if (!list.isEmpty()) {
                    // build packet.
                    WlmrPacket p1 = buildMeterConfigPacket(station, list);
                    sender.get().queuePacket(p1);
                }

                // put into next list
                group = (m1.getDaNo() - 1) / 8;
                list = new ArrayList<>();
                list.add(m1);
            }
        }

        // last group
        if (list.size() > 0) {
            WlmrPacket p1 = buildMeterConfigPacket(station, list);
            ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(p1);
        }
    }

    /**
     * @param station
     * @param msaCfg
     * @param p1
     */
    private WlmrPacket buildMeterConfigPacket(Attribute<MeterStation> station,
            List<MeterConfigBody> list) {
        MsaConfig msaCfg = MsaConfigLoader.getMsaConfig();

        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(msaCfg.getMain().getA1());
        // use station A2.
        r.getAddr().setA2(station.get().getAddr().getA2());
        r.getAddr().getA3().setAddr(msaCfg.getMain().getA3().getAddr());

        r.getControl().setDir(CtrlDir.Down);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.UserData);

        r.setAfn(AfnType.Set);

        r.setPwd(station.get().getPwd());
        r.getFseq().setTpv(true);
        r.getFseq().setCon(true);

        // part1
        Afn04DownPart part1 = new Afn04DownPart();
        part1.getId().getDt().setTypeList(Afn04DownPart.F_METER_CONFIG);

        r.setPart1(part1);

        // unit1
        Afn04F401Unit unit = new Afn04F401Unit();

        unit.setMeterCnt(list.size());
        unit.setMeterList(list);

        part1.firstUnit(unit);

        return r;
    }

    /**
     * Queue the task to query events.
     * 
     * @param ctx
     * @param p
     * @param station
     * @param msaCfg
     */
    private void queueEvents(ChannelHandlerContext ctx, WlmrPacket p) {
        Attribute<PacketSender> sender = ctx.channel().attr(PacketConsts.senderKey);
        MsaConfig msaCfg = MsaConfigLoader.getMsaConfig();

        if (p.getEventCount().getEc1() > 0) {
            WlmrPacket r = new WlmrPacket(log);
            r.getAddr().setA1(msaCfg.getMain().getA1());
            // use station A2.
            r.getAddr().setA2(p.getAddr().getA2());
            r.getAddr().getA3().setAddr(msaCfg.getMain().getA3().getAddr());

            r.getControl().setDir(CtrlDir.Down);
            r.getControl().setPrm(CtrlPrm.Active);
            r.getControl().setFat(FnActiveType.UserData);

            r.getFseq().setCon(true);
            r.getFseq().setTpv(true);

            r.setAfn(AfnType.Third);

            // ec1
            AfnThirdDownPart part1 = new AfnThirdDownPart();
            part1.getId().getDt().setTypeList(AfnThirdDownPart.F_IMPORTANT_EVENT);

            // unit 1
            AfnThirdF1_2DownUnit unit = new AfnThirdF1_2DownUnit();
            unit.setPm((short) 0);
            unit.setPn(p.getEventCount().getEc1());
            part1.firstUnit(unit);

            // add part
            r.setPart1(part1);

            // queue task.
            sender.get().queuePacket(r);
        }

        if (p.getEventCount().getEc2() > 0) {
            WlmrPacket r = new WlmrPacket(log);
            r.getAddr().setA1(msaCfg.getMain().getA1());
            // use station A2.
            r.getAddr().setA2(p.getAddr().getA2());
            r.getAddr().getA3().setAddr(msaCfg.getMain().getA3().getAddr());

            r.getControl().setDir(CtrlDir.Down);
            r.getControl().setPrm(CtrlPrm.Active);
            r.getControl().setFat(FnActiveType.UserData);

            r.getFseq().setCon(true);
            r.getFseq().setTpv(true);

            r.setAfn(AfnType.Third);

            // ec1
            AfnThirdDownPart part1 = new AfnThirdDownPart();
            part1.getId().getDt().setTypeList(AfnThirdDownPart.F_NORMAL_EVENT);

            // unit 1
            AfnThirdF1_2DownUnit unit = new AfnThirdF1_2DownUnit();
            unit.setPm((short) 0);
            unit.setPn(p.getEventCount().getEc1());
            part1.firstUnit(unit);

            // add part
            r.setPart1(part1);

            // queue task.
            sender.get().queuePacket(r);
        }

    }

    /**
     * Just log station info.
     * 
     * @param ctx
     * @param p
     * @param station
     * @param r
     */
    private void dealTerm(ChannelHandlerContext ctx, WlmrPacket p) {
        Afn09UpPart part1 = (Afn09UpPart) p.getPart1();
        Afn09F1UpUnit unit = (Afn09F1UpUnit) part1.firstUnit();

        // log term info.
        log.info("station info for {}: {}", p.getAddr().stationKey(), unit);
    }

    /**
     * Deal Heart request from meter station.
     * 
     * @param ctx
     * @param p
     * @param station
     * @param r
     */
    public void dealHeart(ChannelHandlerContext ctx, WlmrPacket p) {
        MsaConfig msaCfg = MsaConfigLoader.getMsaConfig();
        Attribute<MeterStation> statAttr = ctx.channel().attr(PacketConsts.stationKey);
        MeterStation station = statAttr.get();

        // build reply packet
        WlmrPacket r = new WlmrPacket(log);
        {
            r.getAddr().setA1(msaCfg.getMain().getA1());
            // use station A2.
            r.getAddr().setA2(p.getAddr().getA2());
            // 响应帧MSA应为0.
//            r.getAddr().getA3().setAddr(msaCfg.getMain().getA3().getAddr());

            r.getControl().setDir(CtrlDir.Down);
            r.getControl().setPrm(CtrlPrm.Proactive);
            r.getControl().setPat(FnProactiveType.Confirm);

            // fill link
            r.setAfn(AfnType.Verify);

            // set rseq same as pseq.
            r.getFseq().setSeq(p.getFseq().getSeq());
            r.getFseq().setTpv(true);
            r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));
        }

        // check part1
        Afn02Part up1 = (Afn02Part) p.getPart1();
        short dt = up1.getId().getDt().firstType();

        switch (dt) {
        case Afn02Part.F_LOGIN:

        // login should be first packet, but just log.
        {
            // confirm part
            Afn00Part rp1 = new Afn00Part();
            rp1.getId().getDt().setTypeList(station != null
                ? Afn00Part.F_CONFIRM : Afn00Part.F_DENY);
            r.setPart1(rp1);

            // send
            ByteBuf buf = ctx.alloc().buffer();
            r.appendBuf(buf);
            log.info("Down {}: {}\nHex: {}",
                r.getAfn(), r, HexString.bytesToHexStringWithBlank(buf.duplicate()));
            ctx.writeAndFlush(buf);

            // log.
            if (station == null) {
                log.info("Deny login from {}.", p.getAddr().stationKey());
            } else {
                log.info("Confirm login from {}.", p.getAddr().stationKey());
            }

            break;
        }

        // logout should be last packet, but just log.
        case Afn02Part.F_LOGOUT:

        {
            // confirm
            Afn00Part rp1 = new Afn00Part();
            rp1.getId().getDt().setTypeList(Afn00Part.F_CONFIRM);
            r.setPart1(rp1);

            // send
            ByteBuf buf = ctx.alloc().buffer();
            r.appendBuf(buf);
            log.info("Down {}: {}\nHex: {}",
                r.getAfn(), r, HexString.bytesToHexStringWithBlank(buf.duplicate()));
            ctx.writeAndFlush(buf);

            // just log
            log.info("Confirm logout from {}.", p.getAddr().stationKey());

            break;
        }

        case Afn02Part.F_HEART:

        {
            // check term clock.
            Afn02F3Unit unit = (Afn02F3Unit) p.getPart1().firstUnit();

            // build part1
            Afn00Part rp1 = new Afn00Part();
            r.setPart1(rp1);

            if (new Duration(unit.getTermClockJoda(), DateTime.now())
                .getStandardMinutes() > ALLOWED_DIFF_MINUTES) {
                // deny if exceeding allowed time diff
                r.getControl().setPat(FnProactiveType.Deny);

                rp1.getId().getDt().setTypeList(Afn00Part.F_DENY);

                // log.
                log.warn("Deny heart because of clock difference for {}.", p.getAddr().stationKey());
            } else {
                // say OK.
                r.getControl().setPat(FnProactiveType.Confirm);

                rp1.getId().getDt().setTypeList(Afn00Part.F_CONFIRM);

                // log
                log.info("Confirm heart for {}.", p.getAddr().stationKey());
            }

            // write & flush
            ByteBuf buf = ctx.alloc().buffer();
            r.appendBuf(buf);
            log.info("Down {}: {}\nHex: {}",
                r.getAfn(), r, HexString.bytesToHexStringWithBlank(buf.duplicate()));
            ctx.writeAndFlush(buf);

            break;

        }

        default:

        {
            log.warn("I don't recognize this unit F-{} from {}", dt, p.getAddr().stationKey());

            break;
        }
        }
    } // deal heart

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#userEventTriggered(io.netty.channel.ChannelHandlerContext, java.lang.Object)
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // get log for current station
        MeterStation station = ctx.channel().attr(PacketConsts.stationKey).get();
        if (station != null) {
            log = LoggerFactory.getLogger(station.getAddr().stationKey());
        }

        // may load config
        MsaConfigLoader.checkConfigFiles();

        MsaConfig mc = MsaConfigLoader.getMsaConfig();

        if (evt instanceof IdleStateEvent) {
            // gonna request station info or set meter config.
            Attribute<PacketSender> sender = ctx.channel().attr(PacketConsts.senderKey);

            // check if first
            PacketTimes pt = sender.get().nextPacket();

            // Term / Query / First / Second
            if (pt != null) {
                pt.getPacket().getFseq().setSeq(PacketConsts.nextSequence(ctx));
                pt.getPacket().getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

                // passwd may be appended.
                if (station != null) {
                    pt.getPacket().getAddr().setA2(station.getAddr().getA2());

                    if (pt.getPacket().getAfn().isImportant()) {
                        pt.getPacket().setPwd(station.getPwd());
                    }
                }

                // send out
                ctx.writeAndFlush(pt.getPacket());
                sender.get().sendOut(pt);

                // statistics.
                ctx.channel().attr(PacketConsts.counterKey).get().incSendCount();
            } else {
                // check first task.
                Attribute<DateTime> lastQuery = ctx.channel().attr(PacketConsts.lastQueryKey);
                Attribute<DateTime> lastConfigure = ctx.channel().attr(PacketConsts.lastConfigureKey);
                Attribute<DateTime> lastFirstTask = ctx.channel().attr(PacketConsts.lastFirstTaskKey);
                Attribute<DateTime> lastSecondTask = ctx.channel().attr(PacketConsts.lastSecondTaskKey);
                Attribute<DateTime> lastFirstData = ctx.channel().attr(PacketConsts.lastFirstDataKey);
                Attribute<DateTime> lastSecondData = ctx.channel().attr(PacketConsts.lastSecondDataKey);

                // set last configure time
                DateTime now = DateTime.now();

                // check last configure time.
                if (new Duration(lastConfigure.get(), now).getStandardMinutes() > mc.getQueryPeriod()) {
                    // plan to configure meter list.
                    queueSetMeterList(ctx, null);

                    lastConfigure.set(now);
                } else if (new Duration(lastQuery.get(), now).getStandardMinutes() > mc.getQueryPeriod()) {
                    // launch first task every 18 hours.
                    queueQueryMeters(ctx);

                    lastQuery.set(DateTime.now());
                } else if (new Duration(lastFirstTask.get(), now).getStandardMinutes() > mc.getFirstTaskMinutes()) {
                    queueFirstTask(ctx);

                    // set task.
                    lastFirstTask.set(DateTime.now());
                } else if (new Duration(lastSecondTask.get(), now).getStandardMinutes() > mc.getFirstDataMinutes()) {
                    queueSecondTask(ctx);

                    // set task.
                    lastSecondTask.set(DateTime.now());
                } else if (new Duration(lastFirstData.get(), now).getStandardMinutes() > mc.getSecondTaskMinutes()) {
                    queueFirst(ctx);

                    // set data
                    lastFirstData.set(DateTime.now());
                } else if (new Duration(lastSecondData.get(), now).getStandardMinutes() > mc.getSecondDataMinutes()) {
                    queueSecond(ctx);

                    // set data
                    lastSecondData.set(now);
                } else {
                    // queue 1 heart every 2 (plan & send) * 10 * 2 = 40s.
                    if (new Random().nextInt(mc.getHeartPeriod()) == 5) {
                        queueHeart(ctx, station);
                    }
                }
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    private void queueHeart(ChannelHandlerContext ctx, MeterStation station) {
        MsaConfig mc = MsaConfigLoader.getMsaConfig();

        WlmrPacket r = new WlmrPacket(log);

        r.getAddr().setA1(mc.getMain().getA1());
        // use station A2.
        if (station != null) {
            r.getAddr().setA2(station.getAddr().getA2());
        }
        r.getAddr().getA3().setAddr(mc.getMain().getA3().getAddr());

        r.setAfn(AfnType.Heart);

        r.getControl().setDir(CtrlDir.Down);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.Heart);

        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        // part1
        Afn02Part part1 = new Afn02Part();
        part1.getId().getDt().setTypeList(Afn02Part.F_HEART);

        r.setPart1(part1);

        // unit
        Afn02F3Unit u3 = new Afn02F3Unit();
        part1.firstUnit(u3);

        ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(r);
    }

    private void queueSecondTask(ChannelHandlerContext ctx) {
        MsaConfig cfg = MsaConfigLoader.getMsaConfig();

        MeterStation station = ctx.channel().attr(PacketConsts.stationKey).get();

        if (station == null) {
            log.warn("Second Task not queued - station not login yet for {}", ctx.name());
            return;
        }

        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(cfg.getMain().getA1());
        r.getAddr().setA2(station.getAddr().getA2());
        r.getAddr().getA3().setAddr(cfg.getMain().getA3().getAddr());

        r.setAfn(AfnType.Set);
        r.setPwd(station.getPwd());

        r.getControl().setDir(CtrlDir.Down);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.UserData);

        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        Afn04DownPart part1 = new Afn04DownPart();
        // task no: 1
        part1.getId().getDa().setAddrList((short) 1);
        part1.getId().getDt().setTypeList(Afn04DownPart.F_SECOND_RPT);

        // unit
        Afn04F6566Unit u1 = new Afn04F6566Unit();
        u1.setR((byte) 1);
        u1.getRptPeriod().setUnit(PeriodUnitType.Day);
        u1.setStartReportTime(new C1DateTimeType().fillJodaValue(DateTime.now(), true));

        short cnt = 0;
        for (MeterConfigBody m1 : station.getMeterList()) {
            DataIdBody id = new DataIdBody();
            id.getDa().setAddrList(m1.getDaNo());
            id.getDt().setTypeList(Afn04DownPart.F_FIRST_RPT);

            u1.getTaskList().add(id);

            // at most launch first task for 16 meters
            if (++cnt >= 16) {
                break;
            }
        }

        u1.setTaskCnt(cnt);

        // set
        part1.firstUnit(u1);
        r.getPartList().add(part1);

        // plan
        ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(r);
    }

    private void queueFirstTask(ChannelHandlerContext ctx) {
        MsaConfig cfg = MsaConfigLoader.getMsaConfig();

        MeterStation station = ctx.channel().attr(PacketConsts.stationKey).get();

        if (station == null) {
            log.warn("First Task not queued - station not login yet for {}", ctx.name());
            return;
        }

        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(cfg.getMain().getA1());
        r.getAddr().setA2(station.getAddr().getA2());
        r.getAddr().getA3().setAddr(cfg.getMain().getA3().getAddr());

        r.setAfn(AfnType.Set);
        r.setPwd(station.getPwd());

        r.getControl().setDir(CtrlDir.Down);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.UserData);

        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        Afn04DownPart part1 = new Afn04DownPart();
        // task no: 1
        part1.getId().getDa().setAddrList((short) 1);
        part1.getId().getDt().setTypeList(Afn04DownPart.F_FIRST_RPT);

        // unit
        Afn04F6566Unit u1 = new Afn04F6566Unit();
        u1.setR((byte) 1);
        u1.setStartReportTime(new C1DateTimeType().fillJodaValue(DateTime.now(), true));

        short cnt = 0;
        for (MeterConfigBody m1 : station.getMeterList()) {
            DataIdBody id = new DataIdBody();
            id.getDa().setAddrList(m1.getDaNo());
            id.getDt().setTypeList(Afn04DownPart.F_FIRST_RPT);

            u1.getTaskList().add(id);

            // at most launch first task for 16 meters
            if (++cnt >= 16) {
                break;
            }
        }

        u1.setTaskCnt(cnt);

        // set
        part1.firstUnit(u1);
        r.getPartList().add(part1);

        // plan
        ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(r);
    }

    /**
     * Request Second data.
     * 
     * @param ctx
     */
    private void queueSecond(ChannelHandlerContext ctx) {
        MeterStation station = ctx.channel().attr(PacketConsts.stationKey).get();
        if (station == null) {
            log.warn("Fail to queue second - station not login yet: {}", ctx.name());
            return;
        }

        // meter list
        int size = station.getMeterList().size();
        int group = -1;
        List<MeterConfigBody> meterGroup = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            MeterConfigBody m1 = station.getMeterList().get(i);
            if (group != (m1.getDaNo() - 1) / 8) {
                if (!meterGroup.isEmpty()) {
                    // build part.
                    WlmrPacket r = buildSecondPacket(meterGroup, station);
                    ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(r);
                }

                // prepare for next group.
                group = (m1.getDaNo() - 1) / 8;
                meterGroup = new ArrayList<>();
                meterGroup.add(m1);
            } else {
                // same group
                meterGroup.add(m1);
            }
        }

        if (!meterGroup.isEmpty()) {
            WlmrPacket r = buildSecondPacket(meterGroup, station);
            ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(r);
        }
    }

    /**
     * build request.
     * 
     * @param r
     * @param meterGroup
     */
    private WlmrPacket buildSecondPacket(List<MeterConfigBody> meterGroup, MeterStation station) {
        MsaConfig config = MsaConfigLoader.getMsaConfig();

        // packet.
        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(config.getMain().getA1());
        r.getAddr().setA2(station.getAddr().getA2());
        r.getAddr().getA3().setAddr(config.getMain().getA3().getAddr());

        r.setAfn(AfnType.Second);

        r.getControl().setDir(CtrlDir.Down);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.Second);

        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        for (int j = 0; j < meterGroup.size(); j++) {
            // part1
            AfnSecondDownPart part1 = new AfnSecondDownPart();
            part1.getId().getDa().setAddrList(meterGroup.get(j).getDaNo());
            part1.getId().getDt().setTypeList(AfnSecondDownPart.F_FROZEN_DAY, AfnSecondDownPart.F_FROZEN_MONTH);

            r.getPartList().add(part1);

            // tdd
            AfnSecondF409DownUnit u9 = new AfnSecondF409DownUnit();
            u9.setFrozeTdd(StdBcdNumber.fromMeterC20Joda(DateTime.now().minusDays(1)));

            part1.addUnitGroup((short) 0, u9);

            // tdm
            AfnSecondF410DownUnit u10 = new AfnSecondF410DownUnit();
            u10.setFrozeTdm(StdBcdNumber.fromMeterC21Joda(DateTime.now().minusMonths(1)));

            part1.addUnitGroup((short) 0, u10);
        }

        return r;
    }

    /**
     * Request First data.
     * 
     * @param ctx
     */
    private void queueFirst(ChannelHandlerContext ctx) {
        MeterStation station = ctx.channel().attr(PacketConsts.stationKey).get();
        if (station == null) {
            log.warn("Fail to queue First - station not login yet: {}", ctx.name());
            return;
        }

        // meter list
        int size = station.getMeterList().size();
        int group = -1;
        List<MeterConfigBody> meterGroup = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            MeterConfigBody m1 = station.getMeterList().get(i);
            if (group != (m1.getDaNo() - 1) / 8) {
                if (!meterGroup.isEmpty()) {
                    // build part.
                    WlmrPacket r = buildFirstPacket(meterGroup, station);
                    ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(r);
                }

                group = (m1.getDaNo() - 1) / 8;
                meterGroup = new ArrayList<>();
                meterGroup.add(m1);
            } else {
                // same group
                meterGroup.add(m1);
            }
        }

        if (!meterGroup.isEmpty()) {
            WlmrPacket r = buildFirstPacket(meterGroup, station);
            ctx.channel().attr(PacketConsts.senderKey).get().queuePacket(r);
        }
    }

    private WlmrPacket buildFirstPacket(List<MeterConfigBody> meterGroup, MeterStation station) {
        MsaConfig config = MsaConfigLoader.getMsaConfig();

        // packet.
        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(config.getMain().getA1());
        r.getAddr().setA2(station.getAddr().getA2());
        r.getAddr().getA3().setAddr(config.getMain().getA3().getAddr());

        r.setAfn(AfnType.First);

        r.getControl().setDir(CtrlDir.Down);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.First);

        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        short[] daGroup = new short[meterGroup.size()];
        for (int j = 0; j < daGroup.length; j++) {
            daGroup[j] = meterGroup.get(j).getDaNo();
        }

        // part1
        AfnFirstPart part1 = new AfnFirstPart();
        part1.getId().getDa().setAddrList(daGroup);
        part1.getId().getDt().setTypeList(AfnFirstPart.F_METER_STATUS, AfnFirstPart.F_VELOCITY, AfnFirstPart.F_VOLUME);

        r.getPartList().add(part1);

        return r;
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#exceptionCaught(io.netty.channel.ChannelHandlerContext, java.lang.Throwable)
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        MeterStation station = ctx.channel().attr(PacketConsts.stationKey).get();

        log.error("exception during communicating for {} ({}) from {}",
            station == null ? "un-login station" : station.getAddr().stationKey(),
            ctx.name(), ctx.channel().remoteAddress());

        log.info("exception details for " + cause.getMessage(), cause);

        ctx.close();
    }

}
